#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#endregion

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Xml;
using Adoor.Expression;
using Adoor.Expression.AST;
using Adoor.Object.Disconnected;
using Adoor.Object.Domain;
using Adoor.Object.Entity;
using Adoor.Object.Query;
using Adoor.Relational;

namespace Adoor.Object.Relational
{
    public interface IReferenceMappingStrategyProvider
    {
        IReferenceMappingStrategy GetMappingStrategy(MappingStrategyEnum ms);
    }

    public interface IReferenceMappingStrategy
    {
        ExpressionNode GetRelationalExpression(string correlation, string value, string alias);
        RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias, bool partial, EntityData sourceEntity);
        RelationalOperator Cross(RelationalOperator source, RelationalQuery targetQuery, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin);
        RelationalOperator Cross(RelationalOperator source, RelationalOperator source2, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin);
    }

    public class ForeignKeyMapping<TReference> :
        IService, IReferenceMapping
        , ISingleReferenceReader<TReference>
        , IReferenceMappingStrategyProvider
        , IReferenceMappingStrategy
    {
        private ReferenceData referenceData;
        private string foreignKey;
        private Type type;

        public ForeignKeyMapping(ReferenceData referenceData, XmlNode metadata)
        {
            this.referenceData = referenceData;
            
            this.foreignKey = metadata.SelectSingleNode("@foreignKey").Value;
            XmlNode typenamenode = metadata.SelectSingleNode("@type");
            this.type = typenamenode != null ? Type.GetType(typenamenode.Value, true) : typeof(Int32);

            this.ReferenceData.RegisterService(typeof(IReferenceReader), this);
            this.ReferenceData.RegisterService(typeof(IReferenceReader<TReference>), this);
            this.ReferenceData.RegisterService(typeof(ISingleReferenceReader<TReference>), this);
            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategyProvider), this);
            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategy), this);
        }

        public void Initialize()
        {
            this.ReferenceData.RegisterService(typeof(IReferenceMapping), this);
        }

        public ReferenceData ReferenceData
        {
            get { return this.referenceData; }
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias)
        {
            return new JoinOperator(isOuterJoin,
                source, this.foreignKey,
                targetQuery, targetKey.PrimaryKey,
                selector, alias);
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias, bool partial, EntityData sourceEntity)
        {
            return Join(isOuterJoin, source, targetQuery, targetKey, selector, alias);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalQuery targetQuery, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            return new CrossOperator(source, targetQuery, alias, joinCondition, isOuterJoin);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalOperator source2, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            return new CrossOperator(source, source2, alias, joinCondition, isOuterJoin);
        }

        public void InsertDataColumn(IList<DataColumn> dataColumns)
        {

            DataColumn dc = new DataColumn(this.foreignKey, this.type);
            dc.AllowDBNull = true; // TODO: read from XML
            dc.DefaultValue = DBNull.Value;
            dataColumns.Add(dc);
        }

        public virtual TReference GetValue(IEntity sourceEntity)
        {
            object k = ((IRelationalEntityState)sourceEntity.State)[this.foreignKey];
            if (k == null || k is DBNull)
                return default(TReference);
            KeyValue key = new KeyValue(k);
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
            ObjectContext context = storage.Context;
            if (context != null)
                return context.Find<TReference>(this.ReferenceData.TargetData, ((IKeyMappingStrategy)this.ReferenceData.TargetData.OidData).GetOid(key));
            else
                return storage.Find<TReference>(this.ReferenceData.TargetData, ((IKeyMappingStrategy)this.ReferenceData.TargetData.OidData).GetOid(key));
        }

        public IList<TReference> Collect(IEntity sourceEntity)
        {
            TReference targetEntity = GetValue(sourceEntity);
            if (targetEntity == null)
                return new TReference[] { };
            return new TReference[] { targetEntity };
        }

        public System.Collections.IEnumerator EnumTargetKeys(IEntity sourceEntity)
        {
            object key = ((IRelationalEntityState)sourceEntity.State)[this.foreignKey];
            if (key == null || key is DBNull)
                return (new KeyValue[] { }).GetEnumerator();
            return (new KeyValue[] { new KeyValue(key) }).GetEnumerator();
        }

        public void FetchReference(ObjectContext context, ObjectPath path, object args, string root, string alias, bool isGloballyLoaded)
        {
            if (isGloballyLoaded)
            {
                SelectExpression column = new SelectExpression(new ExpressionName("", root), "");
                ObjectQuery query2 = new ObjectQuery(path, new SelectExpression[] { column });
                foreach (IEntity entity in context.LocalStorage.Collect(query2, args))
                {
                    if (entity == null)
                        continue;

                    IEntityFactory factory = entity.State.EntityFactory;
                    
                    entity.State.CompleteReference(this.ReferenceData.TargetRole, PreloadFlags.Reference);
                    //System.Diagnostics.Trace.WriteLine("Mark " + this.ReferenceData.SourceData.EntityType.Name + "(" + entity.State.Oid + ")." + this.ReferenceData.TargetRole, "SQL");
                }
            }
        }

        public void FetchTargetEntities(IEntity sourceEntity)
        {
            GetValue(sourceEntity);
        }

        public void InitInverseReferenceReader<TInverse>(ReferenceData inverseReferenceData)
        {
            throw new NotImplementedException();
        }

        public string ForeignKey
        {
            get { return this.foreignKey; }
        }

        public IReferenceMappingStrategy GetMappingStrategy(MappingStrategyEnum ms)
        {
            return this;
        }
        public ExpressionNode GetRelationalExpression(string correlation, string value, string alias)
        {
            return new ExpressionName(correlation, value);
        }
    }

    public class ForeignKeyFactory<TReference> : 
        ForeignKeyMapping<TReference>
        , ISingleReferenceWriter<TReference>, INullableBackReference  
    {
        public ForeignKeyFactory(ReferenceData referenceData, XmlNode metadata)
            :
            base(referenceData, metadata)
        {
            this.ReferenceData.RegisterService(typeof(IReferenceWriter), this);
            this.ReferenceData.RegisterService(typeof(IReferenceWriter<TReference>), this);
            this.ReferenceData.RegisterService(typeof(ISingleReferenceWriter<TReference>), this);
            //should we inverse in foreignKey
            //this.ReferenceData.SourceData.Domain.ObjectDependencies.Add(this.ReferenceData.SourceData, new DependantEntity( this.ReferenceData.TargetData));  
            this.ReferenceData.SourceData.Domain.ObjectDependencies.Add(this.ReferenceData.TargetData, new DependantEntity(this.ReferenceData.SourceData,this));  
        }

         //parentFactory;
        private object parentFactory;
        public void InitInverseReferenceWriter<TInverse>(ReferenceData inverseReferenceData)
        {
          this.parentFactory =  new InverseForeignKeyFactory<TInverse, TReference>(inverseReferenceData, this);
        }

        private bool aggregate = false;  // TODO: not used
        public bool Aggregate
        {
            get { return this.aggregate; }
        }

        public virtual void SetNullValue(IEntity sourceEntity)
        {
            //EntityData ed = this.ReferenceData.SourceData;
            //    ed.Domain.ObjectDependencies[ed][0].      
            SetValue(sourceEntity, null); 
        }

        public virtual void SetValue(IEntity sourceEntity, IEntity targetEntity)
        {
            object targetKey;
            if (targetEntity == null)
                targetKey = DBNull.Value;
            else
            {
                IRelationalEntityState targetState = (IRelationalEntityState)targetEntity.State;
                KeyValue key = targetState.Key;
                
                if (targetEntity.State.Storage != sourceEntity.State.Storage)
                    throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name + "'");
                targetKey = key.Values[0];
            }
            IRelationalEntityState sourceState = (IRelationalEntityState)sourceEntity.State;
            if (!sourceState[this.ForeignKey].Equals(targetKey))
                sourceState[this.ForeignKey] = targetKey;
        }

        public void Clear(IObjectStorage localStore)
        {
            // nothing
        }

        public void Update(ObjectContext context)
        {
            // nothing
        }

        public void Commit(ObjectContext context)
        {
            // nothing
        }

        public void Cancel(IObjectStorage localStore)
        {
            // nothing
        }

        public void Remove(IEntity sourceEntity, IEntity targetEntity)
        {
            if (targetEntity.State.Storage != sourceEntity.State.Storage)
                throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name + "'");

            if (object.Equals(GetValue(sourceEntity), targetEntity))
                SetValue(sourceEntity, null);
        }

        public void Add(IEntity sourceEntity, IEntity targetEntity
            , bool checkForIntegrity)
        {
            if (targetEntity != null && targetEntity.State.Storage != sourceEntity.State.Storage)
                throw new Exception("storage mismatch source '" + sourceEntity.State.Storage.Name + "' target '" + targetEntity.State.Storage.Name + "'");
            if (checkForIntegrity && GetValue(sourceEntity) != null)
                throw new Exception("Cannot add entity to the reference");
            SetValue(sourceEntity, targetEntity);
        }

        public new IList<TReference> Collect(IEntity sourceEntity)
        {
            List<TReference> result = new List<TReference>();
            TReference targetEntity = GetValue(sourceEntity);
            if (targetEntity != null)
                result.Add(targetEntity);
            return result;
        }
    }

    public class InverseForeignKeyFactory<TReference, TInverse> :
        IService, IReferenceMapping,
        IReferenceReader<TReference>, ISingleReferenceReader<TReference>,
        IReferenceWriter<TReference>, ISingleReferenceWriter<TReference>,
        IReferenceMappingStrategyProvider, IReferenceMappingStrategy
    {
        private ReferenceData referenceData;
        private ForeignKeyFactory<TInverse> parentFactory;

        public InverseForeignKeyFactory(ReferenceData referenceData
            , ForeignKeyFactory<TInverse> parentFactory)
        {
            this.referenceData = referenceData;
            this.parentFactory = parentFactory;

            this.ReferenceData.RegisterService(typeof(IReferenceReader), this);
            this.ReferenceData.RegisterService(typeof(IReferenceReader<TReference>), this);
            this.ReferenceData.RegisterService(typeof(IReferenceWriter), this);
            this.ReferenceData.RegisterService(typeof(IReferenceWriter<TReference>), this);
            if (this.ReferenceData.IsSingleReference)
            {
                this.ReferenceData.RegisterService(typeof(ISingleReferenceReader<TReference>), this);
                this.ReferenceData.RegisterService(typeof(ISingleReferenceWriter<TReference>), this);
            }
            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategyProvider), this);
            this.ReferenceData.RegisterService(typeof(IReferenceMappingStrategy), this);
        }

        public void Initialize()
        {
            this.ReferenceData.RegisterService(typeof(IReferenceMapping), this);
        }

        public ReferenceData ReferenceData
        {
            get { return this.referenceData; }
        }

        public bool Aggregate
        {
            get { return false; }
        }

        public void InsertDataColumn(IList<DataColumn> dataColumns)
        {
            // nothing
        }

        public void Clear(IObjectStorage localStore)
        {
            // nothing
        }

        public void Cancel(IObjectStorage localStore)
        {
            // nothing
        }

        public void Update(ObjectContext context)
        {
            // nothing
        }

        public void Commit(ObjectContext context)
        {
            // nothing
        }

        public IList<TReference> Collect(IEntity sourceEntity)
        {
            return new ReferenceList<TReference>(this.ReferenceData, sourceEntity);
        }

        public void Add(IEntity sourceEntity, IEntity targetEntity, bool checkForIntegrity)
        {
            if (targetEntity == null)
                throw new ArgumentNullException("targetEntity");
            if (checkForIntegrity && this.parentFactory.GetValue(targetEntity) != null)
                throw new Exception("constraint violation");
            
            this.parentFactory.SetValue(targetEntity, sourceEntity);
        }

        public void Remove(IEntity sourceEntity, IEntity targetEntity)
        {
            if (targetEntity == null)
                throw new ArgumentNullException("targetEntity");
            if (!object.Equals(this.parentFactory.GetValue(targetEntity), sourceEntity))
                throw new Exception("constraint violation");
            this.parentFactory.SetValue(targetEntity, null);
        }

        public void InitInverseReferenceReader<TInverse2>(ReferenceData inverseReferenceData)
        {
            throw new NotSupportedException("InverseForeignKeyFactory.GetInverseReferenceReader should never be called...");
        }

        public void InitInverseReferenceWriter<TInverse2>(ReferenceData inverseReferenceData)
        {
            throw new NotSupportedException("InverseForeignKeyFactory.GetInverseStoredFactory should never be called...");
        }

        public System.Collections.IEnumerator EnumTargetKeys(
            IEntity sourceEntity)
        {
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;
            string mult = this.ReferenceData.SourceToTargetMultiplicity;
            ObjectPath op;

            if (mult == "1")
            {
                string targetRole = parentFactory.ReferenceData.TargetRole;
                string filterRole = sourceEntity.State.EntityFactory.EntityData.EntityType.Name;
                if (targetRole.ToLower() != filterRole.ToLower())
                    targetRole += "!" + filterRole + " as " + targetRole;

                op = this.ReferenceData.TargetData.GetPath(
                    this.ReferenceData.TargetData.EntityType.Name
                    ).Join("(" + this.ReferenceData.TargetData.EntityType.Name + "." + targetRole + ")").Where(parentFactory.ReferenceData.TargetRole + "=?");
            }
            else
            {
                op = this.ReferenceData.TargetData.GetPath(
                    this.ReferenceData.TargetData.EntityType.Name
                    ).Join("(" + this.ReferenceData.TargetData.EntityType.Name + "." + parentFactory.ReferenceData.TargetRole + ")").Where(parentFactory.ReferenceData.TargetRole + "=?");
            }
            //op.ExcludedCorrelations = new string[] { parentFactory.TargetRole };
            SelectExpression column = new SelectExpression(new ExpressionName("", this.ReferenceData.TargetData.EntityType.Name), "");
            ObjectQuery query = new ObjectQuery(op, new SelectExpression[] { column });

            PreloadFlags flags = PreloadFlags.Reference;

            ObjectContext context = storage.Context;
            if (context != null && !context.Disconnected && !sourceEntity.State.IsReferenceComplete(this.ReferenceData.TargetRole, ref flags))
            {
                context.Preload(op, new object[] { sourceEntity });
                // mark as globally loaded
                sourceEntity.State.CompleteReference(this.ReferenceData.TargetRole, PreloadFlags.Reference | PreloadFlags.TargetEntities);
                //System.Diagnostics.Trace.WriteLine("Mark " + this.ReferenceData.SourceData.EntityType.Name + "(" + sourceEntity.State.Oid + ")." + this.ReferenceData.TargetRole, "SQL");
            }

            // TODO: there must be something to optimize here!
            System.Collections.IList result = new System.Collections.ArrayList();
            foreach (IEntity entity in storage.Collect(query, new object[] { sourceEntity }))
                result.Add(((IRelationalEntityState)entity.State).Key);
            return result.GetEnumerator();
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias)
        {
            IEntityMappingStrategyProvider provider = (IEntityMappingStrategyProvider)this.parentFactory.ReferenceData.TargetData.GetService(typeof(IEntityMappingStrategyProvider));
            IEntityMappingStrategy mappingStrategy = provider.GetMappingStrategy(MappingStrategyEnum.Remote2); // TODO : remove MappingStrategyEnum.Remote
            //Debug.Assert(targetKey.DataColumns.Length != 1 || targetKey.PrimaryKey == this.parentFactory.ForeignKey);
            return new JoinOperator(isOuterJoin,
                source, mappingStrategy.KeyMappingStrategy.PrimaryKey,
                targetQuery, this.parentFactory.ForeignKey,  // targetKey.PrimaryKey does not work here for composite keys
                selector, alias);
        }

        public RelationalOperator Join(bool isOuterJoin, RelationalOperator source, RelationalQuery targetQuery, IKeyMappingStrategy targetKey, string selector, string alias, bool partial, EntityData sourceEntity)
        {
            return Join(isOuterJoin, source, targetQuery, targetKey, selector, alias);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalQuery targetQuery, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            return new CrossOperator(source, targetQuery, alias, joinCondition, isOuterJoin);
        }

        public RelationalOperator Cross(RelationalOperator source, RelationalOperator source2, string correlation, string alias, ExpressionNode joinCondition, bool isOuterJoin)
        {
            return new CrossOperator(source, source2, alias, joinCondition, isOuterJoin);
        }

        public void FetchReference(ObjectContext context, ObjectPath path, object args, string root, string alias, bool isGloballyLoaded)
        {
            if (isGloballyLoaded)
            {
                SelectExpression column = new SelectExpression(new ExpressionName("", root), "");
                ObjectQuery query2 = new ObjectQuery(path, new SelectExpression[] { column });
                foreach (IEntity entity in context.LocalStorage.Collect(query2, args))
                {
                    if (entity == null)
                        continue;

                    IEntityFactory factory = entity.State.EntityFactory;
                    
                    entity.State.CompleteReference(this.ReferenceData.TargetRole, PreloadFlags.Reference);
                    //System.Diagnostics.Trace.WriteLine("Mark " + this.ReferenceData.SourceData.EntityType.Name + "(" + entity.State.Oid + ")." + this.ReferenceData.TargetRole, "SQL");
                }
            }
        }

        public void FetchTargetEntities(IEntity sourceEntity)
        {
            DataSetObjectStorage storage = (DataSetObjectStorage)sourceEntity.State.Storage;

            PreloadFlags flags = PreloadFlags.Reference;
            ObjectContext context = storage.Context;
            if (context != null && !context.Disconnected && !sourceEntity.State.IsReferenceComplete(this.ReferenceData.TargetRole, ref flags))
            {
                ObjectPath path = this.ReferenceData.TargetData.GetPath(
                    this.ReferenceData.TargetData.EntityType.Name
                    ).Join("(" + this.ReferenceData.TargetData.EntityType.Name + "." + parentFactory.ReferenceData.TargetRole + ")").Where(parentFactory.ReferenceData.TargetRole + "=?");
                //path.ExcludedCorrelations = new string[] { parentFactory.TargetRole };
                context.Preload(path, new object[] { sourceEntity });
            }
        }

        public IReferenceMappingStrategy GetMappingStrategy(MappingStrategyEnum ms)
        {
            return this;
        }

        public TReference GetValue(IEntity sourceEntity)
        {
            ICollection<TReference> c = this.Collect(sourceEntity);
            IEnumerator<TReference> enumerator = c.GetEnumerator();
            if (!enumerator.MoveNext())
                return default(TReference);
            TReference result = enumerator.Current;
            if (enumerator.MoveNext())
                throw new Exception("constraint violation: too many entities found");
            return result;
        }

        public void SetValue(IEntity sourceEntity, IEntity targetEntity)
        {
            object current = this.GetValue(sourceEntity);
            if (current != null)
                this.Remove(sourceEntity, (IEntity)current);
            if (targetEntity != null)
                this.Add(sourceEntity, targetEntity, false);
        }

        public ExpressionNode GetRelationalExpression(string correlation, string value, string alias)
        {
            return this.parentFactory.GetRelationalExpression(correlation, value, alias);
        }
    }
}
